Skip to content

feat: add service-scoped env to Foundry extensions - #9079

Merged
huimiu merged 34 commits into
mainfrom
hui/update-foundry-extension-env
Aug 4, 2026
Merged

feat: add service-scoped env to Foundry extensions#9079
huimiu merged 34 commits into
mainfrom
hui/update-foundry-extension-env

Conversation

@huimiu

@huimiu huimiu commented Jul 10, 2026

Copy link
Copy Markdown
Member

Summary

Foundry extensions (agent, connection, routine, toolbox) now read service-scoped environment values from ServiceConfig.Environment instead of the project-wide azd environment, and write generated variables into the standard services.<name>.env block.

  • Each target prefers the forwarded service environment; services without env keep the old project-wide behavior.
  • Fixed two timing bugs where extensions read environment values before the user was prompted for them.
  • Replaced the agents extension's two independent ${VAR} scanners with one shared scanner.
  • Updated schemas, examples, minimum azd versions, and SDK dependencies for azd 1.27.1.

Follow-up to #8936. Fixes: #9231

Why this change

Service scoping. azd core now expands each service's environment before handing it to the extension. Extensions have to consume those values so services stop leaking variables into each other, while projects with no service-level env keep working unchanged.

The empty-value bug. azd up initializes every service target while building the execution graph — before the user is prompted for a missing subscription or location. A config read at that moment holds empty strings, so the user answers the prompt and the extension still deploys a blank value, with no error anywhere. Two fixes: the agent target now uses the config passed to each deploy-time entrypoint instead of the copy cached at Initialize (re-running $ref expansion on it), and the provisioning provider resolves the azd environment before it reads service environments.

One ${VAR} scanner. Writing an env: block means deciding which variables a service references. #9212 answered a near-identical question — which variables to prompt for — with a second implementation, and the two had already drifted:

init prompting generated env: block
Implementation regex hand-written scanner
Nested default ${A:-${B}} inexpressible partly handled
Escaping odd/even $ count any preceding $

They are now one scanner, pinned by a test to foundry.ExpandEnv — the code that actually resolves these values — so it can't drift again. Escaping follows the expander too: $${VAR} is literal, $$${VAR} expands. The only remaining policy difference is one explicit line: init skips a reference that has a default, the env block records it.

Nested references stay out of scope by design: ${OUTER:-${NESTED}} reports OUTER only. foundry.ExpandEnv still resolves NESTED at deploy, but nothing discovers it, so it never reaches the generated env: block and init never prompts for it. That limitation is now stated in the scanner's doc comment and pinned by a test instead of left implicit — keep defaults literal.

Environment terminology

Term Meaning
Process environment Inherited by a locally launched agent. Highest precedence, with command-owned values.
Active azd environment Project-wide values. Modern services use these only for required platform values; services without env also use them as the legacy fallback.
Service env Raw templates under services.<name>.env. Commands use the project config API to preserve them on disk.
Forwarded service environment Service-scoped, core-expanded literals from ServiceConfig.Environment. Consumed as-is, never expanded again.
Agent definition environment Deprecated inline environmentVariables or legacy agent.yaml. Service env wins when both exist.

Template syntax has distinct owners: ${VAR} is resolved by azd, ${{...}} by Foundry, and $${{...}} preserves a Foundry expression through azd expansion.

Manual Testing

  • azd ai agent init — migrated legacy environmentVariables to env, preserved ${VAR:-default}, escaped Foundry templates as $${{...}}.
  • azd ai agent run — service env resolved for the child process while the template stayed intact on disk; legacy inline definitions resolved through the azd fallback.
  • azd ai agent optimize apply — raw templates and scalars preserved through field-level updates.
  • azd deploy — toolbox endpoint resolved from service env, and from the azd fallback for a legacy toolbox.

@github-actions github-actions Bot added ext-agents azure.ai.agents extension ext-connections azure.ai.connections extension ext-foundry azure.ai.{agents,connections,inspector,projects,routines,skills,toolboxes}, microsoft.foundry ext-routines azure.ai.routines extension ext-toolboxes azure.ai.toolboxes extension labels Jul 10, 2026
@huimiu
huimiu marked this pull request as ready for review July 10, 2026 12:16
Copilot AI review requested due to automatic review settings July 10, 2026 12:16

This comment was marked as outdated.

jongio

This comment was marked as outdated.

@huimiu huimiu changed the title fix: use core env in Foundry service targets refactor: use core env in Foundry service targets Jul 14, 2026
Co-authored-by: huimiu <107838226+huimiu@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 14, 2026 09:54
@huimiu
huimiu removed the request for review from Copilot July 14, 2026 09:54
Co-authored-by: huimiu <107838226+huimiu@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 14, 2026 09:59
@huimiu
huimiu removed the request for review from Copilot July 14, 2026 09:59
jongio

This comment was marked as outdated.

Copilot AI review requested due to automatic review settings July 14, 2026 10:33

@jongio jongio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seven threads from my earlier passes are still open on this HEAD with no replies. Listing them so they don't get lost now that the PR is showing approved.

  • env_refs.go resolveVars divergence: I added detail on that thread. The :- half fails silently, and a value the user was prompted for ends up in the Bicep param as literal text.
  • helpers.go the service env read swallows the RPC error and leaves hasServiceEnvironment false, so the run path falls back to the full azd environment, which is the leak this PR closes. Routines and toolboxes return the error instead.
  • agent_definition.go ResolveAgentEnvironmentVariable falls through to the caller mapping for any name a declared env: doesn't carry, and both callers hand it the full azd environment.
  • resource_services.go setServiceEnvironment returns before writing when collection comes back empty, so a service with no ${VAR} references gets no env: key and reads as legacy at run time.
  • env_refs.go environmentReferenceAt never checks that the byte after $ is {. "$HOME}" reports OME while foundry.ExpandEnv leaves the value untouched, which is the exact invariant TestFindEnvironmentReferencesMatchesExpander asserts.
  • env_refs_test.go every value in that parity corpus has $ followed by {, so it can't catch the case above.
  • env_refs_test.go the TestNestedDefaultIsNotDiscovered comment says the outer name reaches init prompting, but the assertion four lines down is require.Empty.

None of these are new asks, they're the same threads. The two scanner ones and the two test ones are narrow. The three that fall back to the full azd environment are the ones I'd want settled before this merges, since they all undo the isolation the PR is adding.

This comment was marked as outdated.

This comment was marked as outdated.

This comment was marked as outdated.

@trangevi trangevi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving, pending addressing Jon's comments, and with a possible suggestion to consider now or as a follow up, if it would indeed make things easier.

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/env_refs.go Outdated
jongio

This comment was marked as outdated.

This comment was marked as outdated.

# Conflicts:
#	cli/azd/extensions/azure.ai.agents/README.md

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 44 out of 44 changed files in this pull request and generated 1 comment.

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/resource_services.go Outdated

@jongio jongio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One gap left over from the empty-env change.

  • init.go:2960 and init_from_code.go:873: agent init still goes through the guarded setServiceEnvironment, so an agent with no collected variables gets no env: key and falls back to the full azd environment at run time.

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/init.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 45 out of 45 changed files in this pull request and generated 5 comments.

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go
Comment thread cli/azd/extensions/azure.ai.routines/internal/cmd/service_target.go
Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/optimize_apply.go

@jongio jongio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One issue with the empty-env fix in fc54476.

HIGH

  • setServiceEnvironmentScope sends env: {} over the config RPC, but core drops it before it reaches azure.yaml, so a generated agent service with no variables of its own still reads back as legacy and inherits the whole azd environment. Details inline on resource_services.go.

Both init call sites now route through the new function and the tests do pin that, so the gap isn't in this commit. It's one layer down, in how ServiceConfig.Environment is serialized.

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/resource_services.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 44 out of 44 changed files in this pull request and generated no new comments.

@jongio jongio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental change since my last review checks out. The unpersistable env: {} write is gone, both init paths and addResourceService route through setServiceEnvironment, and the comment records why an explicit empty env can't survive the save today. Build, vet, and the agents cmd suite pass locally against 88a6b76.

One optional follow-up: this behavior flipped twice in the PR, so a small test pinning that an empty environment writes no env: key would keep it from coming back by accident. resource_services_test.go is the natural home now that TestAddResourceServiceWritesEmptyEnvironment is gone.

@jongio jongio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified against 88a6b76: the shared ${VAR} scanner (escape collapsing so $${VAR} stays literal while $$${VAR} expands, balanced-default depth counting, and the probe-based check that keeps Foundry ${{...}} spans reserved), the Initialize reordering that resolves the azd environment before reading service environments, and the run-time precedence chain: process env, then service env, then the FOUNDRY_* platform values, then legacy definition env, with the Windows case-insensitive match preserved.

No blocking issues found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/extensions Extensions (general) ext-agents azure.ai.agents extension ext-connections azure.ai.connections extension ext-foundry azure.ai.{agents,connections,inspector,projects,routines,skills,toolboxes}, microsoft.foundry ext-projects azure.ai.projects extension ext-routines azure.ai.routines extension ext-toolboxes azure.ai.toolboxes extension

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate Foundry extensions to service-level env

5 participants